import { LocalEventService } from '../agStack/events/localEventService';
import type { AgEvent } from '../agStack/interfaces/agEvent';
import type { IAgEventEmitter } from '../agStack/interfaces/iEventEmitter';
import { _exists, _missing } from '../agStack/utils/generic';
import { _escapeString } from '../agStack/utils/string';
import type { ColumnState } from '../columns/columnStateUtils';
import { BeanStub } from '../context/beanStub';
import type { ColumnEvent, ColumnEventType } from '../events';
import { _addGridCommonParams } from '../gridOptionsUtils';
import type {
    Column,
    ColumnEventName,
    ColumnGroup,
    ColumnGroupShowType,
    ColumnHighlightPosition,
    ColumnInstanceId,
    ColumnPinnedType,
    HeaderColumnId,
    ProvidedColumnGroup,
} from '../interfaces/iColumn';
import type { IFrameworkEventListenerService } from '../interfaces/iFrameworkEventListenerService';
import type { IRowNode } from '../interfaces/iRowNode';
import { _mergeDeep } from '../utils/mergeDeep';
import { _warn } from '../validation/logging';
import type { AgColumnGroup } from './agColumnGroup';
import type { AgProvidedColumnGroup } from './agProvidedColumnGroup';
import type {
    AbstractColDef,
    BaseColDefParams,
    ColDef,
    ColSpanParams,
    ColumnFunctionCallbackParams,
    IAggFunc,
    RowSpanParams,
    SortDef,
    SortDirection,
    SortType,
} from './colDef';

const COL_DEF_DEFAULTS: Partial<ColDef> = {
    resizable: true,
    sortable: true,
};

let instanceIdSequence = 0;
export function getNextColInstanceId(): ColumnInstanceId {
    return instanceIdSequence++ as ColumnInstanceId;
}

export function isColumn(col: Column | ColumnGroup | ProvidedColumnGroup): col is AgColumn {
    return col instanceof AgColumn;
}

const DEFAULT_SORTING_ORDER: SortDirection[] = ['asc', 'desc', null];
const DEFAULT_ABSOLUTE_SORTING_ORDER: (SortDef | SortDirection)[] = [
    { type: 'absolute', direction: 'asc' },
    { type: 'absolute', direction: 'desc' },
    null,
];

// Wrapper around a user provide column definition. The grid treats the column definition as ready only.
// This class contains all the runtime information about a column, plus some logic (the definition has no logic).
// This class implements both interfaces ColumnGroupChild and ProvidedColumnGroupChild as the class can
// appear as a child of either the original tree or the displayed tree. However the relevant group classes
// for each type only implements one, as each group can only appear in it's associated tree (eg ProvidedColumnGroup
// can only appear in OriginalColumn tree).
export class AgColumn<TValue = any>
    extends BeanStub<ColumnEventName>
    implements Column, IAgEventEmitter<ColumnEventName>
{
    public readonly isColumn = true as const;

    private frameworkEventListenerService?: IFrameworkEventListenerService<any, any>;

    // used by React (and possibly other frameworks) as key for rendering. also used to
    // identify old vs new columns for destroying cols when no longer used.
    private readonly instanceId = getNextColInstanceId();
    /** Sanitised version of the column id */
    public readonly colIdSanitised: string;

    private actualWidth: any;

    // The measured height of this column's header when autoHeaderHeight is enabled
    private autoHeaderHeight: number | null = null;

    private visible: any;
    public pinned: ColumnPinnedType;
    private left: number | null;
    private oldLeft: number | null;
    public aggFunc: string | IAggFunc | null | undefined;
    private sortDef: SortDef = _getSortDefFromInput();
    private _wasSortExplicitlyRemoved: boolean = false;
    public sortIndex: number | null | undefined;
    public moving = false;
    public resizing = false;
    public menuVisible = false;
    public highlighted: ColumnHighlightPosition | null;
    public formulaRef: string | null = null;

    private lastLeftPinned: boolean = false;
    private firstRightPinned: boolean = false;

    public minWidth: number;
    private maxWidth: number;

    public filterActive = false;

    private readonly colEventSvc: LocalEventService<ColumnEventName> = new LocalEventService();

    private fieldContainsDots: boolean;
    private tooltipFieldContainsDots: boolean;
    public tooltipEnabled = false;

    public rowGroupActive = false;
    public pivotActive = false;
    public aggregationActive = false;
    public flex: number | null = null;

    public parent: AgColumnGroup | null;
    public originalParent: AgProvidedColumnGroup | null;

    constructor(
        public colDef: ColDef<any, TValue>,
        // We do NOT use this anywhere, we just keep a reference. this is to check object equivalence
        // when the user provides an updated list of columns - so we can check if we have a column already
        // existing for a col def. we cannot use the this.colDef as that is the result of a merge.
        // This is used in ColumnFactory
        public userProvidedColDef: ColDef<any, TValue> | null,
        public readonly colId: string,
        private readonly primary: boolean
    ) {
        super();
        this.colIdSanitised = _escapeString(colId)!;
    }

    public override destroy() {
        super.destroy();
        this.beans.rowSpanSvc?.deregister(this);
    }

    public getInstanceId(): ColumnInstanceId {
        return this.instanceId;
    }

    private initState(): void {
        const {
            colDef,
            beans: { sortSvc, pinnedCols, colFlex },
        } = this;

        sortSvc?.initCol(this);

        const hide = colDef.hide;
        if (hide !== undefined) {
            this.visible = !hide;
        } else {
            this.visible = !colDef.initialHide;
        }

        pinnedCols?.initCol(this);

        colFlex?.initCol(this);
    }

    // gets called when user provides an alternative colDef, eg
    public setColDef(
        colDef: ColDef<any, TValue>,
        userProvidedColDef: ColDef<any, TValue> | null,
        source: ColumnEventType
    ): void {
        const colSpanChanged = colDef.spanRows !== this.colDef.spanRows;
        this.colDef = colDef;
        this.userProvidedColDef = userProvidedColDef;
        this.initMinAndMaxWidths();
        this.initDotNotation();
        this.initTooltip();
        if (colSpanChanged) {
            this.beans.rowSpanSvc?.deregister(this);
            this.initRowSpan();
        }
        this.dispatchColEvent('colDefChanged', source);
    }

    public getUserProvidedColDef(): ColDef<any, TValue> | null {
        return this.userProvidedColDef;
    }

    public getParent(): AgColumnGroup | null {
        return this.parent;
    }

    public getOriginalParent(): AgProvidedColumnGroup | null {
        return this.originalParent;
    }

    // this is done after constructor as it uses gridOptionsService
    public postConstruct(): void {
        this.initState();

        this.initMinAndMaxWidths();

        this.resetActualWidth('gridInitializing');

        this.initDotNotation();

        this.initTooltip();

        this.initRowSpan();

        this.addPivotListener();
    }

    private initDotNotation(): void {
        const {
            gos,
            colDef: { field, tooltipField },
        } = this;
        const suppressDotNotation = gos.get('suppressFieldDotNotation');
        this.fieldContainsDots = _exists(field) && field.includes('.') && !suppressDotNotation;
        this.tooltipFieldContainsDots = _exists(tooltipField) && tooltipField.includes('.') && !suppressDotNotation;
    }

    private initMinAndMaxWidths(): void {
        const colDef = this.colDef;

        this.minWidth = colDef.minWidth ?? this.beans.environment.getDefaultColumnMinWidth();
        this.maxWidth = colDef.maxWidth ?? Number.MAX_SAFE_INTEGER;
    }

    private initTooltip(): void {
        this.beans.tooltipSvc?.initCol(this);
    }

    private initRowSpan(): void {
        if (this.colDef.spanRows) {
            this.beans.rowSpanSvc?.register(this);
        }
    }

    private addPivotListener(): void {
        const pivotColDefSvc = this.beans.pivotColDefSvc;
        const pivotValueColumn = this.colDef.pivotValueColumn;
        if (!pivotColDefSvc || !pivotValueColumn) {
            return;
        }

        this.addManagedListeners(pivotValueColumn, {
            colDefChanged: (evt) => {
                const colDef = pivotColDefSvc.recreateColDef(this.colDef);
                this.setColDef(colDef, colDef, evt.source);
            },
        });
    }

    public resetActualWidth(source: ColumnEventType): void {
        const initialWidth = this.calculateColInitialWidth(this.colDef);
        this.setActualWidth(initialWidth, source, true);
    }

    private calculateColInitialWidth(colDef: ColDef): number {
        let width: number;
        const colDefWidth = colDef.width;
        const colDefInitialWidth = colDef.initialWidth;

        if (colDefWidth != null) {
            width = colDefWidth;
        } else if (colDefInitialWidth != null) {
            width = colDefInitialWidth;
        } else {
            width = 200;
        }

        return Math.max(Math.min(width, this.maxWidth), this.minWidth);
    }

    public isEmptyGroup(): boolean {
        return false;
    }

    public isRowGroupDisplayed(colId: string): boolean {
        return this.beans.showRowGroupCols?.isRowGroupDisplayed(this, colId) ?? false;
    }

    public isPrimary(): boolean {
        return this.primary;
    }

    public isFilterAllowed(): boolean {
        // filter defined means it's a string, class or true.
        // if its false, null or undefined then it's false.
        const filterDefined = !!this.colDef.filter;
        return filterDefined;
    }

    public isFieldContainsDots(): boolean {
        return this.fieldContainsDots;
    }

    public isTooltipEnabled(): boolean {
        return this.tooltipEnabled;
    }

    public isTooltipFieldContainsDots(): boolean {
        return this.tooltipFieldContainsDots;
    }

    public getHighlighted(): ColumnHighlightPosition | null {
        return this.highlighted;
    }

    public __addEventListener<T extends ColumnEventName>(
        eventType: T,
        listener: (params: ColumnEvent<T>) => void
    ): void {
        this.colEventSvc.addEventListener(eventType, listener);
    }
    public __removeEventListener<T extends ColumnEventName>(
        eventType: T,
        listener: (params: ColumnEvent<T>) => void
    ): void {
        this.colEventSvc.removeEventListener(eventType, listener);
    }

    /**
     * PUBLIC USE ONLY: for internal use within AG Grid use the `__addEventListener` and `__removeEventListener` methods.
     */
    public override addEventListener<T extends ColumnEventName>(
        eventType: T,
        userListener: (params: ColumnEvent<T>) => void
    ): void {
        this.frameworkEventListenerService = this.beans.frameworkOverrides.createLocalEventListenerWrapper?.(
            this.frameworkEventListenerService,
            this.colEventSvc
        );
        const listener = this.frameworkEventListenerService?.wrap(eventType, userListener) ?? userListener;

        this.colEventSvc.addEventListener(eventType, listener);
    }

    /**
     * PUBLIC USE ONLY: for internal use within AG Grid use the `__addEventListener` and `__removeEventListener` methods.
     */
    public override removeEventListener<T extends ColumnEventName>(
        eventType: T,
        userListener: (params: ColumnEvent<T>) => void
    ): void {
        const listener = this.frameworkEventListenerService?.unwrap(eventType, userListener) ?? userListener;
        this.colEventSvc.removeEventListener(eventType, listener);
    }

    public createColumnFunctionCallbackParams(rowNode: IRowNode): ColumnFunctionCallbackParams {
        return _addGridCommonParams(this.gos, {
            node: rowNode,
            data: rowNode.data,
            column: this,
            colDef: this.colDef,
        });
    }

    public isSuppressNavigable(rowNode: IRowNode): boolean {
        return this.beans.cellNavigation?.isSuppressNavigable(this, rowNode) ?? false;
    }

    public isCellEditable(rowNode: IRowNode): boolean {
        return this.beans.editSvc?.isCellEditable({ rowNode, column: this }) ?? false;
    }

    public isSuppressFillHandle(): boolean {
        return !!this.colDef.suppressFillHandle;
    }

    public isAutoHeight(): boolean {
        return !!this.colDef.autoHeight;
    }

    public isAutoHeaderHeight(): boolean {
        return !!this.colDef.autoHeaderHeight;
    }

    public isRowDrag(rowNode: IRowNode): boolean {
        return this.isColumnFunc(rowNode, this.colDef.rowDrag);
    }

    public isDndSource(rowNode: IRowNode): boolean {
        return this.isColumnFunc(rowNode, this.colDef.dndSource);
    }

    public isCellCheckboxSelection(rowNode: IRowNode): boolean {
        return this.beans.selectionSvc?.isCellCheckboxSelection(this, rowNode) ?? false;
    }

    public isSuppressPaste(rowNode: IRowNode): boolean {
        return this.isColumnFunc(rowNode, this.colDef?.suppressPaste ?? null);
    }

    public isResizable(): boolean {
        return !!this.getColDefValue('resizable');
    }

    /** Get value from ColDef or default if it exists. */
    private getColDefValue<K extends keyof ColDef>(key: K): ColDef[K] {
        return this.colDef[key] ?? COL_DEF_DEFAULTS[key];
    }

    public isColumnFunc(
        rowNode: IRowNode,
        value?: boolean | ((params: ColumnFunctionCallbackParams) => boolean) | null
    ): boolean {
        // if boolean set, then just use it
        if (typeof value === 'boolean') {
            return value;
        }

        // if function, then call the function to find out
        if (typeof value === 'function') {
            const params = this.createColumnFunctionCallbackParams(rowNode);
            const editableFunc = value;
            return editableFunc(params);
        }

        return false;
    }

    private createColumnEvent<T extends ColumnEventName>(type: T, source: ColumnEventType): ColumnEvent<T> {
        return _addGridCommonParams(this.gos, {
            type,
            column: this,
            columns: [this],
            source,
        });
    }

    public isMoving(): boolean {
        return this.moving;
    }

    public getSort(): SortDirection {
        // soft deprecation as of v35 - use getSortDef instead
        return this.sortDef.direction;
    }

    /**
     * Returns null if no sort direction applied
     */
    public getSortDef(): SortDef | null {
        if (!this.sortDef.direction) {
            return null;
        }
        return this.sortDef;
    }

    private getColDefAllowedSortTypes(): SortType[] {
        const res: SortType[] = [];
        const { sort, initialSort } = this.colDef;

        const colDefSortType = sort === null ? sort : _normalizeSortType((sort as SortDef)?.type);
        const colDefInitialSortType =
            initialSort === null ? initialSort : _normalizeSortType((initialSort as SortDef)?.type);

        if (colDefSortType) {
            res.push(colDefSortType);
        }
        if (colDefInitialSortType) {
            res.push(colDefInitialSortType);
        }
        return res;
    }

    public getSortingOrder() {
        const defaultSortingOrder = this.getColDefAllowedSortTypes().includes('absolute')
            ? DEFAULT_ABSOLUTE_SORTING_ORDER
            : DEFAULT_SORTING_ORDER;

        return (this.colDef.sortingOrder ?? this.gos.get('sortingOrder') ?? defaultSortingOrder).map(
            (objOrDirection: unknown) => _getSortDefFromInput(objOrDirection)
        );
    }

    public getAvailableSortTypes() {
        const explicitSortTypesFromSortingOrder = this.getSortingOrder().reduce<string[]>((acc, so) => {
            if (so.direction) {
                acc.push(so.type);
            }
            return acc;
        }, this.getColDefAllowedSortTypes());
        return new Set(explicitSortTypesFromSortingOrder);
    }

    get wasSortExplicitlyRemoved(): boolean {
        return this._wasSortExplicitlyRemoved;
    }

    public setSortDef(sortDef: SortDef, initial = false): void {
        if (!initial) {
            this._wasSortExplicitlyRemoved = !sortDef.direction;
        }
        this.sortDef = sortDef;
    }

    public isSortable(): boolean {
        return !!this.getColDefValue('sortable');
    }

    /** @deprecated v32 use col.getSort() === 'asc */
    public isSortAscending(): boolean {
        return this.getSort() === 'asc';
    }

    /** @deprecated v32 use col.getSort() === 'desc */
    public isSortDescending(): boolean {
        return this.getSort() === 'desc';
    }
    /** @deprecated v32 use col.getSort() === undefined */
    public isSortNone(): boolean {
        return _missing(this.getSort());
    }

    /** @deprecated v32 use col.getSort() !== undefined */
    public isSorting(): boolean {
        return _exists(this.getSort());
    }

    public getSortIndex(): number | null | undefined {
        return this.sortIndex;
    }

    public isMenuVisible(): boolean {
        return this.menuVisible;
    }

    public getAggFunc(): string | IAggFunc | null | undefined {
        return this.aggFunc;
    }

    public getLeft(): number | null {
        return this.left;
    }

    public getOldLeft(): number | null {
        return this.oldLeft;
    }

    public getRight(): number {
        return this.left + this.actualWidth;
    }

    public setLeft(left: number | null, source: ColumnEventType) {
        this.oldLeft = this.left;
        if (this.left !== left) {
            this.left = left;
            this.dispatchColEvent('leftChanged', source);
        }
    }

    public isFilterActive(): boolean {
        return this.filterActive;
    }

    /** @deprecated v33 Use `api.isColumnHovered(column)` instead. */
    public isHovered(): boolean {
        _warn(261);
        return !!this.beans.colHover?.isHovered(this);
    }

    public setFirstRightPinned(firstRightPinned: boolean, source: ColumnEventType): void {
        if (this.firstRightPinned !== firstRightPinned) {
            this.firstRightPinned = firstRightPinned;
            this.dispatchColEvent('firstRightPinnedChanged', source);
        }
    }

    public setLastLeftPinned(lastLeftPinned: boolean, source: ColumnEventType): void {
        if (this.lastLeftPinned !== lastLeftPinned) {
            this.lastLeftPinned = lastLeftPinned;
            this.dispatchColEvent('lastLeftPinnedChanged', source);
        }
    }

    public isFirstRightPinned(): boolean {
        return this.firstRightPinned;
    }

    public isLastLeftPinned(): boolean {
        return this.lastLeftPinned;
    }

    public isPinned(): boolean {
        return this.pinned === 'left' || this.pinned === 'right';
    }

    public isPinnedLeft(): boolean {
        return this.pinned === 'left';
    }

    public isPinnedRight(): boolean {
        return this.pinned === 'right';
    }

    public getPinned(): ColumnPinnedType {
        return this.pinned;
    }

    public setVisible(visible: boolean, source: ColumnEventType): void {
        const newValue = visible === true;
        if (this.visible !== newValue) {
            this.visible = newValue;
            this.dispatchColEvent('visibleChanged', source);
        }
        this.dispatchStateUpdatedEvent('hide');
    }

    public isVisible(): boolean {
        return this.visible;
    }

    public isSpanHeaderHeight(): boolean {
        const colDef = this.getColDef();
        return !colDef.suppressSpanHeaderHeight;
    }

    /**
     * Returns the first parent that is not a padding group.
     */
    public getFirstRealParent(): AgProvidedColumnGroup | null {
        let parent = this.getOriginalParent();
        while (parent?.isPadding()) {
            parent = parent.getOriginalParent();
        }
        return parent;
    }

    public getColumnGroupPaddingInfo(): { numberOfParents: number; isSpanningTotal: boolean } {
        let parent = this.getParent();

        if (!parent?.isPadding()) {
            return { numberOfParents: 0, isSpanningTotal: false };
        }

        const numberOfParents = parent.getPaddingLevel() + 1;
        let isSpanningTotal = true;

        while (parent) {
            if (!parent.isPadding()) {
                isSpanningTotal = false;
                break;
            }
            parent = parent.getParent();
        }

        return { numberOfParents, isSpanningTotal };
    }

    public getColDef(): ColDef<any, TValue> {
        return this.colDef;
    }
    public getDefinition(): AbstractColDef<any, TValue> {
        return this.colDef;
    }

    public getColumnGroupShow(): ColumnGroupShowType | undefined {
        return this.colDef.columnGroupShow;
    }

    public getColId(): string {
        return this.colId;
    }

    public getId(): string {
        return this.colId;
    }

    public getUniqueId(): HeaderColumnId {
        return this.colId as HeaderColumnId;
    }

    public getActualWidth(): number {
        return this.actualWidth;
    }

    public getAutoHeaderHeight(): number | null {
        return this.autoHeaderHeight;
    }

    /** Returns true if the header height has changed */
    public setAutoHeaderHeight(height: number): boolean {
        const changed = height !== this.autoHeaderHeight;
        this.autoHeaderHeight = height;
        return changed;
    }

    private createBaseColDefParams(rowNode: IRowNode): BaseColDefParams {
        const params: BaseColDefParams = _addGridCommonParams(this.gos, {
            node: rowNode,
            data: rowNode.data,
            colDef: this.colDef,
            column: this,
        });
        return params;
    }

    public getColSpan(rowNode: IRowNode): number {
        if (_missing(this.colDef.colSpan)) {
            return 1;
        }
        const params: ColSpanParams = this.createBaseColDefParams(rowNode);
        const colSpan = this.colDef.colSpan(params);
        // colSpan must be number equal to or greater than 1

        return Math.max(colSpan, 1);
    }

    public getRowSpan(rowNode: IRowNode): number {
        if (_missing(this.colDef.rowSpan)) {
            return 1;
        }
        const params: RowSpanParams = this.createBaseColDefParams(rowNode);
        const rowSpan = this.colDef.rowSpan(params);
        // rowSpan must be number equal to or greater than 1

        return Math.max(rowSpan, 1);
    }

    public setActualWidth(actualWidth: number, source: ColumnEventType, silent: boolean = false): void {
        actualWidth = Math.max(actualWidth, this.minWidth);
        actualWidth = Math.min(actualWidth, this.maxWidth);
        if (this.actualWidth !== actualWidth) {
            // disable flex for this column if it was manually resized.
            this.actualWidth = actualWidth;
            if (this.flex != null && source !== 'flex' && source !== 'gridInitializing') {
                this.flex = null;
            }

            if (!silent) {
                this.fireColumnWidthChangedEvent(source);
            }
        }
        this.dispatchStateUpdatedEvent('width');
    }

    public fireColumnWidthChangedEvent(source: ColumnEventType): void {
        this.dispatchColEvent('widthChanged', source);
    }

    public isGreaterThanMax(width: number): boolean {
        return width > this.maxWidth;
    }

    public getMinWidth(): number {
        return this.minWidth;
    }

    public getMaxWidth(): number {
        return this.maxWidth;
    }

    public getFlex(): number | null {
        return this.flex;
    }

    public isRowGroupActive(): boolean {
        return this.rowGroupActive;
    }

    public isPivotActive(): boolean {
        return this.pivotActive;
    }

    public isAnyFunctionActive(): boolean {
        return this.isPivotActive() || this.isRowGroupActive() || this.isValueActive();
    }

    public isAnyFunctionAllowed(): boolean {
        return this.isAllowPivot() || this.isAllowRowGroup() || this.isAllowValue();
    }

    public isValueActive(): boolean {
        return this.aggregationActive;
    }

    public isAllowPivot(): boolean {
        return this.colDef.enablePivot === true;
    }

    public isAllowValue(): boolean {
        return this.colDef.enableValue === true;
    }

    public isAllowRowGroup(): boolean {
        return this.colDef.enableRowGroup === true;
    }

    public isAllowFormula(): boolean {
        return this.colDef.allowFormula === true;
    }

    public dispatchColEvent(type: ColumnEventName, source: ColumnEventType, additionalEventAttributes?: any): void {
        const colEvent = this.createColumnEvent(type, source);
        if (additionalEventAttributes) {
            _mergeDeep(colEvent, additionalEventAttributes);
        }
        this.colEventSvc.dispatchEvent(colEvent);
    }

    public dispatchStateUpdatedEvent(key: keyof ColumnState): void {
        this.colEventSvc.dispatchEvent({
            type: 'columnStateUpdated',
            key,
        } as AgEvent<'columnStateUpdated'>);
    }
}

/**
 * Helper to convert input into SortDef, does normalisation of direction and type.
 *
 * If input is already a valid SortDef, we pluck the direction and type from it.
 * Otherwise, we normalise the direction and type from input.
 */
export function _getSortDefFromInput(input?: unknown): SortDef {
    if (_isSortDefValid(input)) {
        return { direction: input.direction, type: input.type };
    }
    return { direction: _normalizeSortDirection(input), type: _normalizeSortType(input) };
}

export function _isSortDirectionValid(maybeSortDir: unknown): maybeSortDir is SortDirection {
    return maybeSortDir === 'asc' || maybeSortDir === 'desc' || maybeSortDir === null;
}

export function _isSortTypeValid(maybeSortType: unknown): maybeSortType is SortType {
    return maybeSortType === 'default' || maybeSortType === 'absolute';
}

export function _isSortDefValid(maybeSortDef: unknown): maybeSortDef is SortDef {
    if (!maybeSortDef || typeof maybeSortDef !== 'object') {
        return false;
    }

    const maybeSortDefT = maybeSortDef as { type?: unknown; direction?: unknown };
    return _isSortTypeValid(maybeSortDefT.type) && _isSortDirectionValid(maybeSortDefT.direction);
}

export function _areSortDefsEqual(sortDef1: SortDef | null | undefined, sortDef2: SortDef | null | undefined): boolean {
    if (!sortDef1) {
        return sortDef2 ? sortDef2.direction === null : true;
    }
    if (!sortDef2) {
        return sortDef1 ? sortDef1.direction === null : true;
    }

    return sortDef1.type === sortDef2.type && sortDef1.direction === sortDef2.direction;
}

export function _normalizeSortDirection(sortDirectionLike?: unknown): SortDirection {
    return _isSortDirectionValid(sortDirectionLike) ? sortDirectionLike : null;
}

export function _normalizeSortType(sortTypeLike?: unknown): SortType {
    return _isSortTypeValid(sortTypeLike) ? sortTypeLike : 'default';
}
